package jmine.tec.persist.hibernate.filter;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import jmine.tec.utils.collection.Mapper;
import jmine.tec.utils.collection.MapperUtils;

import org.hibernate.EntityMode;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Filters;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.SQLFunctionRegistry;
import org.hibernate.engine.FilterDefinition;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.impl.FilterImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.JoinedSubclassEntityPersister;
import org.hibernate.sql.Template;
import org.hibernate.util.FilterHelper;
import org.hibernate.util.StringHelper;

/**
 * Classe responsavel por injetar filtros em subclasses. Atualmente (versao do hibernate-core 3.3.0.SP1) o hibernate nao trata filtros em
 * subclasses (apenas ignora as anotacoes e definicoes). Atraves do metodo {@link #injectFiltersOnSubclasses(SessionFactoryImplementor)}
 * esta classe pode forcar a ativacao dos filtros nas subclasses, mas apresenta os seguintes problemas:
 * <p>
 * <ul>
 * herancas do tipo 'joined' geram queries erradas, por isso este metodo nao pode ser usado em herancas desse tipo e uma excecao sera
 * lancada
 * </ul>
 * <ul>
 * queries feitas na superclasse nao ativam o uso dos filtros - as queries devem ser feitas sobre as classes que definem os filtros
 * </ul>
 * <ul>
 * por ultimo, como estamos modificando campos privados de uma classe ja publicada, pelo JMM os valores novos podem nao ser "vistos". Um
 * sintoma disso sera algum {@link ArrayIndexOutOfBoundsException} ou ainda queries que nao enxergam as condicoes dos filtros. Este caso
 * deveria ser bem incomum dado que encapsulamos o acesso ao sessionFactory pela beanFactory, mas em todo caso fica documentado.
 * </ul>
 * 
 * @author takeshi
 */
public final class FiltersInjector {

    private static final String CATALOG_MARKER_REGEX = "\\$CATALOG_NAME\\$\\.";

    public static final String CATALOG_MARKER = "$CATALOG_NAME$";

    private static final String SCHEMA_MARKER_REGEX = "\\$SCHEMA_NAME\\$\\.";

    public static final String SCHEMA_MARKER = "$SCHEMA_NAME$";

    /**
     * 
     */
    private FiltersInjector() {
    }

    /**
     * Qualifica os filtros das classes, adicionando o nome do schema e do catalalogo quando presentes
     * 
     * @param implementor {@link SessionFactoryImplementor}
     * @param configuration {@link Configuration}
     * @throws NoSuchFieldException se houver alguma diferenca entre a versao do hibernate em que esta classe foi escrita e a da runtime
     * @throws IllegalAccessException se houver um securityManager que nao permita acessar campos privados
     */
    public static void qualifyTableNamesOnFilters(SessionFactoryImplementor implementor, Configuration configuration)
            throws NoSuchFieldException, IllegalAccessException {
        String schema = configuration.getProperty(Environment.DEFAULT_SCHEMA);
        String catalog = configuration.getProperty(Environment.DEFAULT_CATALOG);
        qualifyTableNamesOnFilters(implementor, schema, catalog);
    }

    /**
     * Cria um mapper que faz o replaceAll da string passada sse replacement nao eh vazio. Devolve o Mapper de identidade cc.
     * 
     * @param pattern a regex
     * @param replacement o replacement
     * @return {@link Mapper}
     */
    private static Mapper<String, String> createReplacer(final String pattern, final String replacement) {
        return new Mapper<String, String>() {

            private String replace = (replacement == null || replacement.length() == 0) ? "" : replacement + ".";

            public String apply(String input) {
                return input.replaceAll(pattern, replace);
            }
        };
    }

    /**
     * Qualifica os filtros das classes, adicionando o nome do schema e do catalalogo quando presentes
     * 
     * @param implementor {@link SessionFactoryImplementor}
     * @param schema o nome do schema ou null ou vazio se nao houver
     * @param catalog o nome do catalog ou null ou vazio se nao houver
     * @throws NoSuchFieldException se houver alguma diferenca entre a versao do hibernate em que esta classe foi escrita e a da runtime
     * @throws IllegalAccessException se houver um securityManager que nao permita acessar campos privados
     */
    public static void qualifyTableNamesOnFilters(SessionFactoryImplementor implementor, String schema, String catalog)
            throws NoSuchFieldException, IllegalAccessException {
        Field filterHelperField = AbstractEntityPersister.class.getDeclaredField("filterHelper");
        filterHelperField.setAccessible(true);
        Field filterConditionsField = FilterHelper.class.getDeclaredField("filterConditions");
        filterConditionsField.setAccessible(true);
        Field filterNamesField = FilterHelper.class.getDeclaredField("filterNames");
        filterNamesField.setAccessible(true);

        @SuppressWarnings("unchecked")
        Collection<ClassMetadata> meta = implementor.getAllClassMetadata().values();

        Mapper<String, String> replacer =
                MapperUtils.chainMapper(createReplacer(SCHEMA_MARKER_REGEX, schema), createReplacer(CATALOG_MARKER_REGEX, catalog));

        for (ClassMetadata classMetadata : meta) {
            if (!(classMetadata instanceof AbstractEntityPersister)) {
                throw new IllegalStateException("classMetadata is not an AbstractEntityPersister");
            }
            FilterHelper helper = (FilterHelper) filterHelperField.get(classMetadata);
            String[] conditions = (String[]) filterConditionsField.get(helper);
            for (int i = 0; i < conditions.length; i++) {
                conditions[i] = replacer.apply(conditions[i]);
            }
        }
    }

    /**
     * Injeta os filtros nas subclasses.
     * 
     * @param implementor o {@link SessionFactoryImplementor} que sera usado
     * @throws NoSuchFieldException se alguma classe do hibernate mudar radicalmente, esta excecao deve ser lancada. Isso, porem, nao eh
     * suficiente para garantir a compatibilidade futura
     * @throws IllegalAccessException se o securityManager estiver ligado e bloquear reflection sobre campos private e final em classes do
     * hibernate, essa excecao sera lancada
     */
    public static void injectFiltersOnSubclasses(SessionFactoryImplementor implementor) throws NoSuchFieldException, IllegalAccessException {
        Map<Class<?>, List<FilterDefinition>> localFilters = extractLocalFilters(implementor);
        Map<Class<?>, List<FilterDefinition>> allFilters = inheritFilters(localFilters);

        Field filterHelperField = AbstractEntityPersister.class.getDeclaredField("filterHelper");
        filterHelperField.setAccessible(true);
        Field filterConditionsField = FilterHelper.class.getDeclaredField("filterConditions");
        filterConditionsField.setAccessible(true);
        Field filterNamesField = FilterHelper.class.getDeclaredField("filterNames");
        filterNamesField.setAccessible(true);

        @SuppressWarnings("unchecked")
        Collection<ClassMetadata> meta = implementor.getAllClassMetadata().values();
        for (ClassMetadata classMetadata : meta) {
            if (!(classMetadata instanceof AbstractEntityPersister)) {
                throw new IllegalStateException("classMetadata is not an AbstractEntityPersister");
            }
            List<FilterDefinition> filtersToInject = allFilters.get(classMetadata.getMappedClass(EntityMode.POJO));
            FilterHelper helper = (FilterHelper) filterHelperField.get(classMetadata);
            injectIfNecessary(filterNamesField, filterConditionsField, filtersToInject, helper, implementor.getDialect(), implementor
                    .getSqlFunctionRegistry(), classMetadata.getMappedClass(EntityMode.POJO), (AbstractEntityPersister) classMetadata);
        }
    }

    /**
     * Faz a injecao propriamente dita usando reflection
     * 
     * @param filterNamesField o {@link Field} que representa os nomes dos filtros
     * @param filterConditionsField o {@link Field} que representa as condicoes
     * @param filtersToInject {@link List} de {@link FilterDefinition} a serem injetadas
     * @param helper {@link FilterHelper} que sera modificado.
     * @param dialect o {@link Dialect} usado na hora de "traduzir" a condicao do filtro
     * @param sqlFunctionRegistry {@link SQLFunctionRegistry} para traduzir o filtro
     * @param type o tipo da classe
     * @param entityPersister o {@link AbstractEntityPersister}
     * @throws IllegalAccessException se o securityManager nao deixar os campos serem alterados
     */
    private static void injectIfNecessary(Field filterNamesField, Field filterConditionsField, List<FilterDefinition> filtersToInject,
            FilterHelper helper, Dialect dialect, SQLFunctionRegistry sqlFunctionRegistry, Class type,
            AbstractEntityPersister entityPersister) throws IllegalAccessException {
        String[] filterNames = (String[]) filterNamesField.get(helper);
        if (filterNames.length > filtersToInject.size()) {
            throw new IllegalStateException("hibernate mapped more filters than we have found. Mapped: " + Arrays.toString(filterNames)
                    + " but found: " + filtersToInject);
        }
        if (containsAll(filterNames, filtersToInject)) {
            return;
        }
        if (entityPersister instanceof JoinedSubclassEntityPersister) {
            throw new IllegalStateException("Cannot inject @Filters on JoinedSubclass: " + type);
        }
        final int n = filtersToInject.size();
        filterNames = new String[n];
        String[] conditions = new String[n];
        for (int i = 0; i < n; i++) {
            final FilterDefinition definition = filtersToInject.get(i);
            filterNames[i] = definition.getFilterName();
            conditions[i] =
                    Template.renderWhereStringTemplate(definition.getDefaultFilterCondition(), FilterImpl.MARKER, dialect,
                            sqlFunctionRegistry);
            conditions[i] = StringHelper.replace(conditions[i], ":", ":" + filterNames[i] + ".");
        }
        filterNamesField.set(helper, filterNames);
        filterConditionsField.set(helper, conditions);
    }

    /**
     * Verifica se todos os filtros a serem injetados jah se encontram no array passado, verificando apenas os nomes dos filtros e assumindo
     * que o hibernate levou em consideracao o 'overwrite'
     * 
     * @param filterNames os nomes dos filtros jah injetados
     * @param filtersToInject os nomes dos filtros a serem injetados
     * @return boolean
     */
    private static boolean containsAll(String[] filterNames, List<FilterDefinition> filtersToInject) {
        if (filterNames.length != filtersToInject.size()) {
            return false;
        }
        final ArrayList<FilterDefinition> copy = new ArrayList<FilterDefinition>(filtersToInject);
        for (FilterDefinition filterDefinition : copy) {
            if (!containsFilterWithName(filterNames, filterDefinition)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Verifica se o filtro passado encontra-se no array passado
     * 
     * @param filterName String[]
     * @param filters {@link FilterDefinition}
     * @return boolean
     */
    private static boolean containsFilterWithName(String[] filterName, FilterDefinition filters) {
        for (String string : filterName) {
            if (string.equals(filters.getFilterName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Extrai os filtros que estao definidos nas classes. O mapa devolvido nao leva em consideracao heranca
     * 
     * @param implementor {@link SessionFactoryImplementor}
     * @return {@link Map}
     */
    private static Map<Class<?>, List<FilterDefinition>> extractLocalFilters(SessionFactoryImplementor implementor) {
        @SuppressWarnings("unchecked")
        Map<String, ClassMetadata> metadata = implementor.getAllClassMetadata();
        Map<Class<?>, List<FilterDefinition>> localFilters = new HashMap<Class<?>, List<FilterDefinition>>();
        for (ClassMetadata cmd : metadata.values()) {
            final Class<?> cls = cmd.getMappedClass(EntityMode.POJO);
            final List<FilterDefinition> list;
            if (cls.isAnnotationPresent(Filter.class)) {
                list = new ArrayList<FilterDefinition>(1);
                Filter ann = cls.getAnnotation(Filter.class);
                final FilterDefinition filterDefinition = implementor.getFilterDefinition(ann.name());
                if (filterDefinition == null) {
                    throw new NullPointerException("filter definition: " + ann.name() + " not found");
                }
                list.add(filterDefinition);
            } else if (cls.isAnnotationPresent(Filters.class)) {
                Filter[] ann = cls.getAnnotation(Filters.class).value();
                list = new ArrayList<FilterDefinition>(ann.length);
                for (Filter filter : ann) {
                    final FilterDefinition filterDefinition;
                    if (!"".equalsIgnoreCase(filter.condition().trim())) {
                        filterDefinition = new FilterDefinition(filter.name(), filter.condition(), new HashMap<String, String>());
                    } else {
                        filterDefinition = implementor.getFilterDefinition(filter.name());
                    }

                    if (filterDefinition == null) {
                        throw new NullPointerException("filter definition: " + filter.name() + " not found");
                    }
                    list.add(filterDefinition);
                }
            } else {
                list = Collections.emptyList();
            }
            if (localFilters.put(cls, list) != null) {
                throw new IllegalStateException("class " + cls + " mapped twice!");
            }
        }
        return localFilters;
    }

    /**
     * Constroi um mapa com todos os filtros que devem ser injetados para cada classe.
     * 
     * @param localFilters os filtros declarados localmente para cada classe
     * @return {@link Map}
     */
    private static Map<Class<?>, List<FilterDefinition>> inheritFilters(Map<Class<?>, List<FilterDefinition>> localFilters) {
        Map<Class<?>, List<FilterDefinition>> map = new HashMap<Class<?>, List<FilterDefinition>>();
        for (Class<?> type : localFilters.keySet()) {
            List<FilterDefinition> list = inheritFilters(localFilters, map, type);
            map.put(type, list);
        }
        return map;
    }

    /**
     * Obtem todos os filtros que devem ser aplicados ao type. Filtros declarados localmente sobrescrevem filtros vindos por heranca
     * 
     * @param localFilters {@link Map} com os filtros locais
     * @param map {@link Map} com um cache dos filtros jah calculados
     * @param type {@link Class}
     * @return {@link List}
     */
    private static List<FilterDefinition> inheritFilters(Map<Class<?>, List<FilterDefinition>> localFilters,
            Map<Class<?>, List<FilterDefinition>> map, Class<?> type) {
        if (type == Object.class) {
            return Collections.<FilterDefinition> emptyList();
        }
        List<FilterDefinition> filters = map.get(type);
        if (filters != null) {
            return new ArrayList<FilterDefinition>(filters);
        }
        filters = new ArrayList<FilterDefinition>(inheritFilters(localFilters, map, type.getSuperclass()));
        final List<FilterDefinition> c = localFilters.get(type);
        if (c != null) {
            filters = overwriteFilters(filters, c);
        }
        map.put(type, filters);
        return filters;
    }

    /**
     * Adiciona os filtros locais aos filtros heradados, sobrescrevendo quando necessario
     * 
     * @param inherited {@link List}
     * @param local {@link List}
     * @return {@link List}
     */
    private static List<FilterDefinition> overwriteFilters(List<FilterDefinition> inherited, List<FilterDefinition> local) {
        List<FilterDefinition> toRet = new ArrayList<FilterDefinition>();
        Map<String, FilterDefinition> copy = new LinkedHashMap<String, FilterDefinition>();
        for (FilterDefinition filterDefinition : local) {
            copy.put(filterDefinition.getFilterName(), filterDefinition);
        }

        for (FilterDefinition filterDefinition : inherited) {
            final FilterDefinition filter = copy.remove(filterDefinition.getFilterName());
            if (filter == null) {
                toRet.add(filterDefinition);
            } else {
                toRet.add(filter);
            }
        }
        toRet.addAll(copy.values());
        return toRet;
    }

}
